package com.xiaoshuidi.cloud.module.contract.util.bill;

import com.xiaoshuidi.cloud.module.contract.util.date.DateUtils;

import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Supplier;

/**
 * 周期账单工具类
 */
@Data
@Builder
public class BillPeriod {
    private LocalDate startTime;
    private LocalDate endTime;
    /**
     * 账单周期是否整月(2月特殊)
     */
    private boolean isWholeMonth;
    private boolean isLast;

    /**
     * 按合同周期是整月 例：2024-4-18 ~ 2024-6-17 为整月周期
     */
    private boolean isCycleWholeMonth;

    private BillPeriod(LocalDate startTime, LocalDate endTime, boolean isWholeMonth) {
        assertNotNull(startTime, endTime);
        this.isWholeMonth = isWholeMonth;
        if (startTime.isAfter(endTime)) {
            this.startTime = endTime;
            this.endTime = startTime;
        } else {
            this.startTime = startTime;
            this.endTime = endTime;
        }
    }

    private BillPeriod(LocalDate startTime, LocalDate endTime, boolean isWholeMonth, boolean isLast) {
        assertNotNull(startTime, endTime);
        this.isWholeMonth = isWholeMonth;
        this.isLast = isLast;
        if (startTime.isAfter(endTime)) {
            this.startTime = endTime;
            this.endTime = startTime;
        } else {
            this.startTime = startTime;
            this.endTime = endTime;
        }
    }

    public BillPeriod(LocalDate startTime, LocalDate endTime, boolean isWholeMonth, boolean isLast, boolean isCycleWholeMonth) {
        if (startTime.isAfter(endTime)) {
            this.startTime = endTime;
            this.endTime = startTime;
        } else {
            this.startTime = startTime;
            this.endTime = endTime;
        }
        this.isWholeMonth = isWholeMonth;
        this.isLast = isLast;
        this.isCycleWholeMonth = isCycleWholeMonth;
    }

    /**
     * 周期账单总费用, 如果有不足月的天数, 按日费用计算
     * 日费用 = endDate所在账单某月的日费用*实际天数
     * 例: 1/10 ~ 4/9, endDate = 3/5, endDate所在月周期是2/10~3/9, 日费用取的是2月的天数, 实际费用 = (账单月费用/28天) * Days.between(2/10, 3/5)
     *
     * @param monthlyAmount
     * @return
     */
    public BigDecimal totalFee(BigDecimal monthlyAmount) {
        return totalFee(startTime, endTime, monthlyAmount, isWholeMonth, isLast);
    }

    /**
     * 账单整月数和剩余天数(账单结束时不足月时)
     * <pre>
     * <b>特别注意!特别注意!特别注意!</b>
     * 情况1: contractStartDate &lt; 2月 &lt; contractEndDate时, 2月28|29号开始的账单结束日期是n月最后一天减1天
     * 例: 2月28|29 ~ 3月30, 2月28 ~ 4月29
     * 情况2: contractStartDate = 2月28号 ~ contractEndDate时, 账单结束日期是2月28号+1个月减1天
     * 例: 2月28|29 ~ 3月27|28, 2月28|29 ~ 4月27|28
     * <b>特别注意!特别注意!特别注意!</b>
     * </pre>
     *
     * @param contractStartDate 合同开始时间
     * @param contractEndDate   合同结束时间
     * @return
     */
    public static MonthAndRemainder monthsAndRemainder(LocalDate contractStartDate, LocalDate contractEndDate) {
        return new MonthAndRemainder(contractStartDate, contractEndDate);
    }

    public MonthAndRemainder monthAndRemainder() {
        return monthsAndRemainder(startTime, endTime);
    }

    /**
     * 账单的结束日期(开始时间+1月-1天)
     * 如果2月在合同周期内(非首月), 并且账单开始时间是2月最后一天, 则此账单结束日期为结束日期当月的最后一天-1天
     * 例: 2/28 ~ 3/30, 2/28 ~ 4/29
     *
     * @param startTime                账单开始时间
     * @param isFebruaryWithinContract 二月是否在合同周期内
     * @param month                    开始时间加几个月
     * @return
     */
    public static LocalDate monthBillEndDate(LocalDate startTime, boolean isFebruaryWithinContract, int month, LocalDate endTime) {
        month = Math.max(1, month);
        LocalDate endDate = startTime.plusMonths(month);
        if (endDate.minusDays(1).isEqual(endTime)) {
            return endDate.minusDays(1);
        }
        if (endDate.getMonthValue() == 3 && isFebruaryWithinContract) {
            if (endDate.getDayOfMonth() == endDate.withMonth(2).lengthOfMonth()) {
                return endDate.withDayOfMonth(endDate.lengthOfMonth() - 1);
            }
        }
        return endDate.minusDays(1);
    }

    /**
     * 按指定月份拆分账单周期
     *
     * @param contractStartTime 合同开始时间
     * @param contractEndTime   合同结束时间
     * @param nMonth    几个月
     * @return
     */
    public static List<BillPeriod> splitByMonth(LocalDate contractStartTime, LocalDate contractEndTime, int nMonth) {
        MonthAndRemainder mr = new MonthAndRemainder(contractStartTime, contractEndTime);
        List<BillPeriod> l = new ArrayList<>(mr.getMonth() / nMonth + 1);
        boolean isFebruaryWithinContract = isFebruaryWithin(contractStartTime, contractEndTime);
        LocalDate lastEnd = contractStartTime;
        int i = 1;
        for (; i < mr.getMonth(); i++) {
            if (i % nMonth == 0) {
                LocalDate tmp = monthBillEndDate(contractStartTime, isFebruaryWithinContract, i, contractEndTime);
                l.add(new BillPeriod(lastEnd, tmp, true));
                lastEnd = tmp.plusDays(1);
            }
        }
        if (i <= mr.getMonth()) {
            LocalDate tmp = monthBillEndDate(contractStartTime, isFebruaryWithinContract, i, contractEndTime);
            l.add(new BillPeriod(lastEnd, tmp, true));
            lastEnd = tmp.plusDays(1);
        }
        if (mr.getRemainder() > 0) {
            l.add(new BillPeriod(lastEnd, contractEndTime, false));
        }
        if (!CollectionUtils.isEmpty(l)) {
            l.get(l.size() - 1).setLast(true);
        }
        return l;
    }

    /**
     * 按自然月月份拆分账单周期
     *
     * @param contractStartTime 合同开始时间
     * @param contractEndTime   合同结束时间
     * @param nMonth    几个月
     * @return
     */
    public static List<BillPeriod> splitByNaturalMonth(LocalDate contractStartTime, LocalDate contractEndTime, int nMonth) {
        // 开始时间月第一天
        LocalDate startMonthFirstDay = contractStartTime.withDayOfMonth(1);
        List<BillPeriod> billPeriodList = new ArrayList<>();
        LocalDate firstDay = startMonthFirstDay;

        // 判断合同周期是否整月
        MonthAndRemainder monthAndRemainder = new MonthAndRemainder(contractStartTime, contractEndTime);
        boolean cycleWholeMonth = false;
        if (monthAndRemainder.getRemainder() == 0){
            cycleWholeMonth = true;
        }

        if (!startMonthFirstDay.equals(contractStartTime)){
            // 开始时间非当月第一天
            // 开始时间次月第一天
            firstDay = contractStartTime.plusMonths(1).withDayOfMonth(1);
            // 首期账单时间：开始时间 ~ 当月月底
            billPeriodList.add(new BillPeriod(contractStartTime, contractStartTime.withDayOfMonth(contractStartTime.lengthOfMonth()), false,false,cycleWholeMonth));
        }
        MonthAndRemainder mr = new MonthAndRemainder(firstDay, contractEndTime);
        int i = 1;
        for (; i <= mr.getMonth(); i++) {
            if (i % nMonth == 0) {
                LocalDate tmp = firstDay.plusMonths(nMonth-1).withDayOfMonth(firstDay.plusMonths(nMonth-1).lengthOfMonth());
                billPeriodList.add(new BillPeriod(firstDay, tmp, true,false,cycleWholeMonth));
                firstDay = tmp.plusDays(1);
            }
        }
        if (mr.getRemainder() > 0) {
            billPeriodList.add(new BillPeriod(firstDay, contractEndTime, false,false,cycleWholeMonth));
        }
        if (!CollectionUtils.isEmpty(billPeriodList)) {
            billPeriodList.get(billPeriodList.size() - 1).setLast(true);
        }
        return billPeriodList;
    }

    /**
     * 二月是否在时间段内(不包括开始月份)
     *
     * @param startTime
     * @param endTime
     * @return
     */
    public static boolean isFebruaryWithin(LocalDate startTime, LocalDate endTime) {
        if (startTime.isAfter(startTime.withMonth(2)) &&
                endTime.compareTo(startTime.plusYears(1).withMonth(3).withDayOfMonth(1))>=0) {
            return true;
        }
        return startTime.isBefore(startTime.withMonth(2)) && endTime.isAfter(endTime.withMonth(2));
    }

    /**
     * 周期账单天数
     *
     * @param startTime
     * @param endTime
     * @return
     */
    public static int totalDays(LocalDate startTime, LocalDate endTime) {
        return exchangeIf(startTime, endTime, (s, e) -> (int) ChronoUnit.DAYS.between(s, e) + 1);
    }

    /**
     * 总费用, 不足月按日费用*天数
     *
     * @param startTime
     * @param endTime
     * @param monthAmount
     * @param isWholeBill 是否完整账单
     * @return
     */
    public static BigDecimal totalFee(LocalDate startTime, LocalDate endTime, BigDecimal monthAmount, boolean isWholeBill, boolean isLast) {
        MonthAndRemainder m = new MonthAndRemainder(startTime, endTime);
        BigDecimal total = monthAmount.multiply(BigDecimal.valueOf(m.getMonth()));
        // 生成合同账单计算金额时, 当二月在合同时间中间时(非首月), 账单结束日期是结束日期所在月份的最后一天, 这个账单在这里会余几天出来
        // 当生成账单时, 账单开始时间不是二月最后一天且有余数时, 余数天数是小于1个月几天, 这里直接算做一个月
        if (isWholeBill && m.getRemainder() >= endTime.withMonth(2).lengthOfMonth()) {
            return total.add(monthAmount);
        }
        // 非完整账单计算金额时, 按实际月份和余数计算金额
        if (!isWholeBill) {
            if (m.getMonth() == 0) {
                return monthAmount.multiply(BigDecimal.valueOf(totalDays(startTime, endTime)))
                        .divide(BigDecimal.valueOf(startTime.lengthOfMonth()), 2, RoundingMode.HALF_UP);
            }
            LocalDate remindStart = monthBillEndDate(startTime, m.isFebruaryIn(), m.getMonth(), endTime).plusDays(1);
            if (remindStart.isAfter(endTime)) {
                return total;
            }
            int lengthOfMonth = remindStart.lengthOfMonth();
            total = total.add(monthAmount.multiply(BigDecimal.valueOf(totalDays(remindStart, endTime)))
                    .divide(BigDecimal.valueOf(lengthOfMonth), 2, RoundingMode.HALF_UP));
        }
        return total;
    }

    private static void assertNotNull(LocalDate startTime, LocalDate endTime) {
        Assert.notNull(startTime, "startTime cannot be null");
        Assert.notNull(endTime, "startTime cannot be null");
    }

    private static <T> T exchangeIf(LocalDate left, LocalDate right, BiFunction<LocalDate, LocalDate, T> consumer) {
        assertNotNull(left, right);
        if (left.isAfter(right)) {
            return consumer.apply(right, left);
        } else {
            return consumer.apply(left, right);
        }
    }

    public boolean isEquals(BillPeriod other) {
        if (this == other) return false;
        return startTime.isEqual(other.startTime) && endTime.isEqual(other.getEndTime());
    }

    public static class MonthAndRemainder {
        @Getter
        private int month;
        @Getter
        private int remainder;
        @Getter
        private boolean isFebruaryIn;

        /**
         * @param startDate
         * @param endDate
         */
        public MonthAndRemainder(LocalDate startDate, LocalDate endDate) {
            isFebruaryIn = isFebruaryWithin(startDate, endDate);
            exchangeIf(startDate, endDate, (s, e) -> {
                LocalDate monthEnd = monthBillEndDate(s, isFebruaryIn, 1, endDate);
                // 按账单逻辑刚好1整月
                if (monthEnd.compareTo(e) == 0) {
                    month = 1;
                    return null;
                    // 不足1个月
                } else if (e.compareTo(monthEnd) < 0) {
                    remainder = totalDays(s, e);
                    // 大于1个月
                } else {
                    LocalDate tmp;
                    for (int i = 1; ; i++) {
                        tmp = monthBillEndDate(s, isFebruaryIn, i, endDate);
                        if (tmp.compareTo(e) == 0) {
                            month++;
                            break;
                        } else if (tmp.compareTo(e) > 0) {
                            remainder = totalDays(monthBillEndDate(s, isFebruaryIn, i - 1, endDate).plusDays(1), e);
                            break;
                        }
                        month++;
                    }
                }
                return null;
            });
        }

        public int getYear() {
            return month / 12;
        }

        public boolean isEquals(MonthAndRemainder other) {
            if (other == null) {
                return false;
            }
            return month == other.getMonth() && remainder == other.getRemainder();
        }
    }
}
